/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package logmatcher.analyzers;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 *  This class is a sequence of both text messages and modifiers.<p>Sequence has two options which must be
 *  considered: EXPECT, ORDERED.</p>
 *   <p>The EXPECT option is by default set to  TRUE. It means that the main list is the list of messages expected
 *  to come. If the EXPECT option is set to FALSE it means that the main list is the list of messages which should
 *  never come, otherwise something is wrong.</p>
 *   <p>The ORDERED option is by default set to TRUE. ORDERED sequence waits for the first message, then for
 *  the second one, and so on. If first message never comes, then no matter if the rest messages has come, the
 *  sequence is considered failed. On the contrary, if ORDERED is set to false, the sequence analyzes all
 *  configured text messages each time a log message comes. When all messages have come, no matter in what
 *  sequence, the sequence is completed.</p>
 *   <p>Currently supported modifiers are IN and AFTER.</p>
 *   <p>The IN modifier enables checking if the next text message comes IN specified time. The AFTER modifier
 *  enables checking if the next message comes not before, but AFTER specified time.</p>
 */
public final class Sequence implements Serializable {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    /** Class logger. */
    private static final Log LOGGER = LogFactory.getLog(Sequence.class);

    //~ Instance fields ------------------------------------------------------------------------------------------------

    /** <code>true</code> if this is an ordered sequence. */
    private boolean ordered = true;

    /** <code>true</code> if this is a sequence of expected messages. */
    private boolean expect = true;

    /** Current position of text message in list, valid only for ordered sequences. */
    private int currentTextPosition;

    /** Sequence unique id. */
    private String id;

    /** Sequence of elements. */
    private List<SequenceElement> includeSequence = new ArrayList<SequenceElement>();

    /** Sequence of elements. */
    private List<SequenceElement> withoutSequence = new ArrayList<SequenceElement>();

    /** State of the analysis. */
    private State state = State.NOT_STARTED;

    /** The total time of analysis. */
    private long allTime;
    
    private Analyzer analyzer;

    //~ Constructors ---------------------------------------------------------------------------------------------------

     
/**
     * Creates a new Sequence object.
     */
    public Sequence() {
        this(true, true);        
    }


/**
    * Creates a new Sequence object.
    * @param expect the expect type of the sequence
    * @param ordered the ordered type of the sequence
    */
    public Sequence(boolean expect, boolean ordered) {
        LOGGER.debug("Sequence constructor");

        if (!expect && ordered) {
            LOGGER.debug("Sequence cannot be of type notexpect and ordered.");
            throw new IllegalStateException("Sequence cannot be of type notexpect and ordered.");
        }

        this.ordered = ordered;
        this.expect = expect;
    }

    //~ Methods --------------------------------------------------------------------------------------------------------


    /**
     *  Sets the total time for this sequence to finish.
     *
     *  @param allTime the total time for this sequence
     *
     *  @throws IllegalStateException thrown when modifying already started sequence
     */
    public void setAllTime(long allTime) {
        if (getState() != State.NOT_STARTED) {
            LOGGER.debug("Cannot modify already started sequence.");
            throw new IllegalStateException("Cannot modify already started sequence.");
        }

        this.allTime = allTime;
    }


    /**
     *  Adds text element to the sequence.
     *
     *  @param message message to be added, either text message of modifier
     */
    public void addMessage(String message) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("addMessage, expectedMessage = " + message);
    	}

        if ((message != null) && !message.equals("")) {
            addMessage(new SequenceElement(message));
        }
    }


    /**
     *  Adds text element to the sequence.
     *
     *  @param message message to be added, either text message of modifier
     *  @param count how many times this message should occur
     */
    public void addMessage(String message, int count) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("addMessage, expectedMessage = " + message);
    	}

        if ((message != null) && !message.equals("")) {
            addMessage(new SequenceElement(message, count));
        }
    }


    /**
     *  Adds without message to the sequence.
     *
     *  @param message message to be added
     */
    public void addWithout(String message) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("addWithout(String) = " + message);
    	}

        if ((message != null) && !message.equals("")) {
            addWithout(new SequenceElement(message));
        }
    }


    /**
     *  Adds an element to the sequence.
     *
     *  @param SequenceElement element to be added
     *
     *  @throws IllegalStateException thrown when adding to already started sequence.
     */
    public void addMessage(SequenceElement SequenceElement) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("addToSequence, sequenceElement = " + SequenceElement);
    	}

        if (getState() != State.NOT_STARTED) {
            LOGGER.debug("Cannot add anything to already started sequence.");
            throw new IllegalStateException("Cannot add anything to already started sequence.");
        }

        if (SequenceElement != null) {
            includeSequence.add(SequenceElement);
        }
    }

   /**
     *  Adds without element to the sequence.
     *
     *  @param SequenceElement text element to be added
     *
     *  @throws IllegalStateException thrown when adding to already started sequence
     */
    public void addWithout(SequenceElement SequenceElement) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("addWithout(SequenceElement) = " + SequenceElement);
    	}

        if (getState() != State.NOT_STARTED) {
            LOGGER.debug("Cannot add anything to already started sequence.");
            throw new IllegalStateException("Cannot add anything to already started sequence.");
        }

        if (!expect) {
            LOGGER.debug("Cannot add without message to not expect sequence.");
            throw new IllegalStateException("Cannot add without message to not expect sequence.");
        }

        if (SequenceElement != null) {
            withoutSequence.add(SequenceElement);
        }
    }


    /**
     *  Returns size of the sequence, including SequenceElements and ModifierElements.
     *
     *  @return size of the sequence.
     */
    public int size() {
        return includeSequence.size();
    }


    /**
     *  Starts the sequence. Used when allTime is specified for the sequence, otherwise does
     *  nothing.
     */
    public void start() {
        if (state != State.NOT_STARTED) {
            LOGGER.warn("Sequence has already been started, ignoring");

            return;
        }

        state = State.ONGOING;

        if (allTime > 0) {
            Timer timer = new Timer();
            TimerTask task = new TimerTask() {
                    @Override
                    public void run() {
                        LOGGER.debug("Checking allTime, sequence id = " + id);

                        if (getState() == State.ONGOING) {
                        	if(LOGGER.isDebugEnabled()){
                                LOGGER.debug("Sequence id = " + id + ", time is up, sequence NOT yet COMPLETED, it means failure");
                        	}
                            setResult(State.FAILURE);
                        } else {
                        	if(LOGGER.isDebugEnabled()){
                                LOGGER.debug("Sequence id = " + id + ", time is up, sequence already COMPLETED");
                        	}                            
                        }
                    }
                };

            timer.schedule(task, allTime);
        }

        LOGGER.debug("Sequence started");
    }


    /**
     *  Stops the sequence. If it is still ongoing then the sequence assumes the result is SUCCESS.
     */
    public synchronized void stop() {
        if (state == State.NOT_STARTED) {
            LOGGER.warn("Sequence not started yet, ignoring");

            return;
        }

        if ((state == State.FAILURE) || (state == State.SUCCESS)) {
            LOGGER.warn("Sequence already finished, ignoring");

            return;
        }

        if(LOGGER.isDebugEnabled()){
            LOGGER.debug("Stopping sequence, id = " + id);
        }

        if (expect) {
            setResult(State.FAILURE);
        } else {
            setResult(State.SUCCESS);
        }
    }


    /**
     *  Checks a message against the sequence.
     *
     *  @param message message to analyze.
     */
    public synchronized void check(String message) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("check, id = " + id + ", message = " + message);
    	}

        if (state == State.NOT_STARTED) {
            LOGGER.debug("sequence not started yet, ignoring");

            return;
        }

        if ((state == State.FAILURE) || (state == State.SUCCESS)) {
            LOGGER.debug("sequence already finished, ignoring");

            return;
        }

        if (ordered) {
            checkOrdered(message);
        } else {
            checkUnordered(message);
        }
    }


    /**
     *  Checks ordered sequence.
     *
     *  @param message message to be checked.
     */
    private synchronized void checkOrdered(String message) {
        LOGGER.debug("Checking ordered sequence");

        if (withoutSequence.size() > 0) {
            checkWithoutSequence(message);
        }

        //check state again
        if (getState() != State.ONGOING) {
            LOGGER.debug("Sequence already completed, not checking any further.");

            return;
        }

        SequenceElement se = includeSequence.get(currentTextPosition);

        if (se == null) {
            LOGGER.debug("no more elements, return");

            return;
        } 

        boolean found = se.check(message);

        if (found && (state == State.ONGOING)) {
            if (expect) {
                currentTextPosition++;
            } else { //not expect
                LOGGER.debug("it is the unexpected message, failed");
                setResult(State.FAILURE);
            }
        }

        if (currentTextPosition == includeSequence.size()) {
            LOGGER.debug("maxPosition reached, sequence completed");
            setResult(State.SUCCESS);
        }
    }


    /**
     *  Checks the without sequence against the incoming message.
     *
     *  @param message message to be checked.
     */
    private synchronized void checkWithoutSequence(String message) {
        LOGGER.debug("Checking without sequence");

        for (int i = 0; i < withoutSequence.size(); i++) {
            SequenceElement se = (SequenceElement)withoutSequence.get(i);

            if (se.check(message)) {
                LOGGER.debug("Without message found, sequence failed");
                setResult(State.FAILURE);
            }
        }
    }


    /**
     *  Checks unordered sequence.
     *
     *  @param message message to be checked.
     */
    private synchronized void checkUnordered(String message) {
        LOGGER.debug("checking unordered sequence");

        if (expect && (withoutSequence.size() > 0)) {
            checkWithoutSequence(message);
        }

        //check state again
        if (getState() != State.ONGOING) {
            LOGGER.debug("sequence already completed, not checking any further.");

            return;
        }

        if (expect) {
            checkUnorderedExpect(message);
        } else {
            checkUnorderedNotExpect(message);
        }
    }


    /**
     *  Checks the unordered sequence of type expect.
     *
     *  @param message the message to check
     */
    private synchronized void checkUnorderedExpect(String message) {
        LOGGER.debug("checking unordered expect sequence");

        SequenceElement se;

        for (int i = 0; i < includeSequence.size(); i++) {
            se = includeSequence.get(i);

            if (se != null) {
                if (se.check(message)) {
                    includeSequence.remove(se);

                    break;
                }
            }
        }

        if (includeSequence.size() == 0) {
            LOGGER.debug("size = 0, sequence completed");
            setResult(State.SUCCESS);
        }
    }


    /**
     *  Checks the unordered sequence of type notexpect.
     *
     *  @param message the message to be checked
     */
    private synchronized void checkUnorderedNotExpect(String message) {
        LOGGER.debug("checking unordered notexpect sequence");

        SequenceElement se;

        for (int i = 0; i < includeSequence.size(); i++) {
            se = includeSequence.get(i);

            if (se != null) {
                if (se.check(message)) {
                    LOGGER.debug("found not expected message, FAILURE");
                    setResult(State.FAILURE);

                    break;
                }
            }
        }
    }


    /**
     *  Tells if the sequence is complete, either successfully or not.
     *
     *  @return true if the sequence is complete, false if not
     */
    public boolean isFinished() {
        if ((getState() == State.SUCCESS) || (getState() == State.FAILURE)) {
        	if(LOGGER.isDebugEnabled()){
                LOGGER.debug("isFinished, return true for id = " + id);
        	}

            return true;
        }

        if(LOGGER.isDebugEnabled()){
            LOGGER.debug("isFinished, return false for id = " + id);
        }

        return false;
    }


    /**
     *  Tells if the result is successful.
     *
     *  @return true if result is successful, false otherwise
     */
    public boolean isSuccess() {
        if (getState() == State.SUCCESS) {
            LOGGER.debug("isSuccess, return true for id = " + id);

            return true;
        }

        if(LOGGER.isDebugEnabled()){
            LOGGER.debug("isSuccess, return false for id = " + id);
        }

        return false;
    }


    /**
     *  Sets the result and notifies all threads waiting for this sequence to be completed.
     *
     *  @param state the state of the analysis.
     */
    private synchronized void setResult(State state) {
    	if(LOGGER.isDebugEnabled()){
            LOGGER.debug("Result = " + state);
    	}
        this.state = state;
        if(analyzer != null)
        analyzer.sequenceCompleted(this);
    }


    /**
     *  Checks the result of the analysis. If the analysis is not complete yet then the calling thread is
     *  blocked until the  sequence completes.
     *
     *  @return the state of the analysis
     */
    public synchronized State checkResult() {
        return state;
    }


    /**
     *  Builds a string containing all messages in the sequence, with marking around the message
     *  currently awaited.
     *
     *  @return string contaiting all messages in the sequence
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");

        for (int i = 0; i < includeSequence.size(); i++) {
            SequenceElement se = includeSequence.get(i);

            if (se instanceof SequenceElement) {
                if (i == currentTextPosition) {
                    builder.append("-->");
                }

                SequenceElement s = (SequenceElement)se;
                builder.append(s);

                if (i == currentTextPosition) {
                    builder.append("<--");
                }

                if (i < (includeSequence.size() - 1)) {
                    builder.append(",");
                }
            }
        }

        builder.append("]");

        return builder.toString();
    }


    /**
     *  Checks if this is an expect sequence.
     *
     *  @return true if this is an expect sequence, false otherwise.
     */
    public boolean isExpect() {
        return expect;
    }


    /**
     *  Returns current state.
     *
     *  @return current state
     */
    public synchronized State getState() {
        return state;
    }


    /**
     *  Checks if this is an ordered sequence.
     *
     *  @return true if this is an ordered sequence, false otherwise.
     */
    public boolean isOrdered() {
        return ordered;
    }


    /**
     *  Creates a deep copy of the original sequence and sets the id to the copy.
     *
     *  @param id to be set to the copy of the sequence
     *
     *  @return copied sequence with id set
     */
    public Object cloneWithId(String id) {
        Sequence copy = (Sequence)SerializationUtils.clone(this);
        copy.id = id;
        copy.analyzer = analyzer;

        return copy;
    }


    /**
     * Returns id of the sequence.
     *
     *  @return id of the sequence
     */
    public String getId() {
        return id;
    }

    //~ Inner Classes --------------------------------------------------------------------------------------------------

     /**
     *  Class of SequenceElement. Simply wraps up the String message.
     */
    class SequenceElement implements Serializable {
        //~ Instance fields --------------------------------------------------------------------------------------------

        /** The message. */
        private String message;

        /** Counter. */
        private int counter;

        //~ Constructors -----------------------------------------------------------------------------------------------

/**
        *  Creates a new SequenceElement object.
        *
        *  @param message the message
        */
        public SequenceElement(String message) {
            this(message, 1);
        }


/**
        *  Creates a new SequenceElement object.
        *
        *  @param message the message
        *  @param counter how many times the message should occur
        */
        public SequenceElement(String message, int counter) {
            this.message = message;
            this.counter = counter;
        }

        //~ Methods ----------------------------------------------------------------------------------------------------

        /**
         *  Returns a string containing the wrapped message.
         *
         *  @return string containing the wrapped message
         */
        @Override
        public String toString() {
            return message;
        }


        /**
         *  Checks if the received message matches the configured one.
         *
         *  @param receivedMessage the message to be checked.
         *
         *  @return true if the message matches, false if not.
         */
        public boolean check(Object receivedMessage) {
            boolean found = false;

            if (((String)receivedMessage).contains(message)) {
            	if(LOGGER.isDebugEnabled()){
                    LOGGER.debug("SequenceElement('" + message + "'), checking... - MATCHED");
            	}
                counter--;
                found = true;
            } else {
            	if(LOGGER.isDebugEnabled()){
                    LOGGER.debug("SequenceElement('" + message + "'), checking... NOT MATCHED");
            	}
            }

            if (found && (counter == 0)) {
            	if(LOGGER.isDebugEnabled()){
                    LOGGER.debug("SequenceElement('" + message + "'), checking completed, counter = 0");
            	}

                return true;
            }

            return false;
        }
    }

    //~ Enumeration initializers ---------------------------------------------------------------------------------------
/**
     *
     * State of the sequence.
     *
     */
    enum State {//~ Enumeration constant initializers --------------------------------------------------------------------------

        FAILURE,NOT_STARTED,
        ONGOING,
        SUCCESS;
    }

public Analyzer getAnalyzer() {
	return analyzer;
}


public void setAnalyzer(Analyzer analyzer) {
	this.analyzer = analyzer;
}
}
